#!/usr/bin/python

# mail2shell 2 daemon
# $0 user:pass@pop.free.fr ## OBSOLETE
# $0 mail2host.conf

### conf exemple ###
## [General]
## user=pop_user
## password=pop_password
## pop=pop.myisp.tld
## # gpg --list-public-keys --with-colons :
## sender_key=09B14785CC44E886
## smtp=smtp.myisp.tld



import os,sys,email,email.FeedParser,ConfigParser,tempfile,smtplib
from poplib import POP3
from pprint import pprint
from time import sleep
from threading import Thread,Timer
from popen2 import Popen4
import GPG

body_pending="""A thread has stopped writing to stdout/stderr. Maybe it's waiting for input.
Here's the output of this thread :
"""

body_ended="""The execution of sended command is finished. Here's the output produced :
"""

class treatMail(Thread):
   def __init__ (self,msg,cfg):
      Thread.__init__(self)
      self.setDaemon(True)
      self.msg = msg
      self.cfg = cfg
      self.status = -1
      self.output=[]

   def isSigned(self,msg):
      """check if a message is signed with gpg"""
      if (msg.is_multipart()):
         # arg, mime, not treated
         return {'valid':0}
      obj = GPG.GPGSubprocess()
      sig = obj.verify( msg.get_payload() )
      return sig.__dict__

   def sendEmail (self,server,fro,to,subject,body,body_header):
      "send result email"
      msg = ("From: %s\nTo: %s\nSubject: Execution %s\n\n%s"
               %(fro,to,subject,body_header+"".join(body)))
      s = smtplib.SMTP(server)
      #~ s.set_debuglevel(1)
      s.sendmail(fro, to, msg)
      s.quit()
   
   def addEchos(self,str):
      """double each command with an echo of it"""
      ret=""
      for line in str.split('\n'):
         ret += "echo \"> "+line+"\"\n"+line+"\n"
      return ret
   
   def threadPendingForever(self):
      """thread will be considered as dead"""
      print self.getName(),"waiting for too long, ending"
      self.output.append("\n> execution too long KILLING\n")
      for pid in Popen4("ps -o pid --no-headers --ppid "+str(self.child.pid)).fromchild.readlines():
         os.kill(int(pid.strip()),15)
         os.kill(int(pid.strip()),9)
      os.kill(self.child.pid,15)
      os.kill(self.child.pid,9)
   
   def threadPending(self):
      """send email to user if thread is pending, asking for input"""
      print self.getName(),"pending",self.msg['Subject']
      self.sendEmail(self.cfg['smtp_host'],self.msg['To'],self.msg['From'],
                     self.getName()+" pending : "+self.msg['Subject'],
                     self.output,body_pending)
   
   def execCmds (self,cmds):
      """Execute a list of shell commands"""
      # write commands to temp file
      f = tempfile.NamedTemporaryFile(dir='/tmp/')
      f.write(self.addEchos(cmds))
      f.flush()
      #~ os.system ("cat "+f.name)
      # exec commands
      # start a timer for max exec time
      bigT = Timer(self.cfg['execution'],self.threadPendingForever)
      bigT.start()
      self.child = Popen4("bash "+f.name)
      while True:
         t = Timer(self.cfg['timeout'],self.threadPending)
         t.start()
         line = self.child.fromchild.readline()
         t.cancel()
         if not line:
            break
         self.output.append(line)
      bigT.cancel()
      return self.output

   def sendInput (self,msg):
      """send input from msg to sdtin"""
      # verify signature
      d = self.isSigned(msg)
      if d['valid'] == 1 and d['key_id'] == self.cfg['sender_key']:
         print self.getName(),"Message input '%s' valide, sending ..."%(msg['Subject'])
         self.child.tochild.write(d['data'])
         self.child.tochild.flush()

   def run(self):
      d = self.isSigned(self.msg)
      if d['valid'] == 1 and d['key_id'] == self.cfg['sender_key']:
         print self.getName(),"Message '%s' valide, execution ..."%(self.msg['Subject'])
         #~ pprint(d)
         retour = self.execCmds(d['data'])
         #~ print retour
         self.sendEmail(self.cfg['smtp_host'],self.msg['To'],self.msg['From'],
                        self.msg['Subject'],
                        retour,body_ended)
         print self.getName(),"Ended"
      else:
         print self.getName(),"Message '%s' INvalide"%(self.msg['Subject'])


class controlApp:
   def __init__(self,cfg):
      self.cfg = cfg
      self.childs=[]
      
   def run(self):
      while True:
         # get mails and display them
         print "Connection au serveur pop ..."
         msgs = self.getMails(self.cfg['user'],self.cfg['passwd'],self.cfg['pop_host'])
         print len(msgs)," messages"
         self.treatMails(msgs)
         print "Dors ..."
         sleep(float(self.cfg['delay']))
   
   def treatMails(self,msgs):
      # check validity of messages
      print "Parcours des messages ..."
      existing_thread = False
      for msg in msgs:
         for thread in self.childs:
            if msg['Subject'].find(thread.getName()) != -1:
               # find existing thread name in subject, this is an input
               existing_thread = True
               print "find existing thread",thread.getName()
               thread.sendInput(msg)
         if not existing_thread:
            # new command
            current = treatMail(msg,self.cfg)
            self.childs.append(current) # todo : howto remove child from childlist ?
            current.start()
         
   def getMails(self,user,passwd,host):
      """return list of messages (email objects)"""
      l=[]
      srv = POP3(host)
      srv.user(user)
      srv.pass_(passwd)
      srv.list()
      (numMsgs, totalSize) = srv.stat()
      for i in range(1, numMsgs + 1):
         (header, msg, octets) = srv.retr(i)
         srv.dele(i) # delete mail
         fp = email.FeedParser.FeedParser()
         fp.feed("\n".join(msg)+"\n")
         l.append(fp.close())
      srv.quit()
      return l

   def displayMails(msgs):
      """Display messages from getMails"""
      for msg in msgs:
         print "From: ",msg['From']
         print "Subject: ",msg['Subject']
         print msg.get_payload()
         print '-----------------------'
   

if __name__ == "__main__":
   
   # get user/pass/server ## OBSOLETE
   #~ user=sys.argv[1].split(':')[0]
   #~ passwd=sys.argv[1].split(':')[1].split('@')[0]
   #~ host=sys.argv[1].split('@')[1]
   
   if len(sys.argv) < 2:
      print "usage: %s <config_file>"%sys.argv[0]
      sys.exit(2)
   cfg_dict={}
   cfg = ConfigParser.ConfigParser()
   cfg.read(sys.argv[1])
   cfg_dict['user']=cfg.get("General","user")
   cfg_dict['passwd']=cfg.get("General","password")
   cfg_dict['pop_host']=cfg.get("General","pop")
   cfg_dict['sender_key']=cfg.get("General","sender_key")
   cfg_dict['smtp_host']=cfg.get("General","smtp")
   cfg_dict['delay']=float(cfg.get("General","delay"))
   cfg_dict['timeout']=float(cfg.get("General","timeout"))
   cfg_dict['execution']=float(cfg.get("General","execution"))
   
   controlApp(cfg_dict).run()

